﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
using AntDesign.Core.JsInterop.Modules.Components;
using AntDesign.JsInterop;
using Microsoft.AspNetCore.Components;

namespace AntDesign.Internal
{
    public sealed partial class Overlay : AntDomComponentBase
    {
        [CascadingParameter(Name = "ArrowPointAtCenter")]
        public bool ArrowPointAtCenter { get; set; }

        /// <summary>
        /// Used in nested overlays (for example menu -> submenu) when
        /// trigger is another overlay.
        /// </summary>
        [CascadingParameter(Name = "ParentTrigger")]
        public OverlayTrigger ParentTrigger { get; set; }

        /// <summary>
        /// Component that will trigger the overlay to show.
        /// </summary>
        [CascadingParameter(Name = "Trigger")]
        public OverlayTrigger Trigger { get; set; }

        [Parameter]
        public RenderFragment ChildContent { get; set; }

        [Parameter]
        public EventCallback OnOverlayMouseEnter { get; set; }

        [Parameter]
        public EventCallback OnOverlayMouseLeave { get; set; }

        [Parameter]
        public EventCallback OnOverlayMouseUp { get; set; }

        [Parameter]
        public EventCallback OnShow { get; set; }

        [Parameter]
        public EventCallback OnHide { get; set; }

        [Parameter]
        public string OverlayChildPrefixCls { get; set; } = "";

        [Parameter]
        public int HideMillisecondsDelay { get; set; } = 100;

        [Parameter]
        public int WaitForHideAnimMilliseconds { get; set; } = 200;

        /// <summary>
        /// vertical offset between Trigger and Overlay, default is 4
        /// </summary>
        [Parameter]
        public int VerticalOffset { get; set; } = 4;

        /// <summary>
        /// horizontal offset between Trigger and Overlay, default is 4
        /// </summary>
        [Parameter]
        public int HorizontalOffset { get; set; } = 4;

        /// <summary>
        /// By default Overlay does not render its content if Overlay hasn't been
        /// activated (shown at least once). Setting HiddenMode = true will
        /// go through rendering process.
        /// Use case: Select component, when using <see cref="SimpleSelectOption"/> or <see cref="SelectOption{TItemValue, TItem}"/>
        /// needs HiddenMode = true, so the select options are initialized and
        /// potential defaults can be rendered properly.
        /// </summary>
        [Parameter]
        public bool HiddenMode { get; set; } = false;

        [Inject]
        private IDomEventListener DomEventListener { get; set; }

        private bool _hasAddOverlayToBody = false;
        private bool _isPreventHide = false;
        private List<Overlay> _childrenToShow = new();
        private bool _mouseInOverlay = false;

        private bool _isOverlayFirstRender = true;
        private bool _isWaitForOverlayFirstRender = false;

        private bool _preVisible = false;
        private bool _isOverlayDuringShowing = false;
        private bool _isOverlayShow = false;
        private bool _isOverlayHiding = false;
        private bool _lastDisabledState = false;

        private int? _overlayLeft = null;
        private int? _overlayTop = null;

        //if this style needs to be changed, also change
        //the removal of that style in js interop overlay.ts class (in constructor)
        private string _overlayStyle = "display: none;"; //initial value prevents from screen flickering when adding overlay to dom; it will be overwritten immediately

        private string _overlayCls = "";

        private bool _shouldRender = true;
        private bool _afterFirstRender = false;

        protected override bool ShouldRender()
        {
            if (_shouldRender)
                return base.ShouldRender();
            _shouldRender = true;
            return false;
        }

        protected override void OnInitialized()
        {
            _overlayCls = Trigger.GetOverlayHiddenClass();
            base.OnInitialized();
        }

        protected override async Task OnParametersSetAsync()
        {
            if (!_isOverlayShow && Trigger.Visible && !_preVisible)
            {
                await Show(_overlayLeft, _overlayTop);
            }
            else if (_isOverlayShow && !Trigger.Visible && _preVisible)
            {
                await Hide(true);
            }

            _preVisible = Trigger.Visible;
            await base.OnParametersSetAsync();
        }

        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                _afterFirstRender = true;
                DomEventListener.AddShared<JsonElement>("window", "beforeunload", Reloading);
            }

            if (_lastDisabledState != Trigger.Disabled)
            {
                if (Ref.Id != null)
                {
                    if (Trigger.Disabled)
                    {
                        await JsInvokeAsync(JSInteropConstants.AddClsToFirstChild, Ref, $"disabled");
                    }
                    else
                    {
                        await JsInvokeAsync(JSInteropConstants.RemoveClsFromFirstChild, Ref, $"disabled");
                    }
                }
                _lastDisabledState = Trigger.Disabled;
            }

            if (_isWaitForOverlayFirstRender && _isOverlayFirstRender)
            {
                _isOverlayFirstRender = false;
                //await Show(_overlayLeft, _overlayTop);

                _isWaitForOverlayFirstRender = false;
                _overlayLeft = null;
                _overlayTop = null;
            }
            await base.OnAfterRenderAsync(firstRender);
        }

        protected override void Dispose(bool disposing)
        {
            if (_hasAddOverlayToBody && !_isReloading)
            {
                _ = InvokeAsync(async () =>
                {
                    await Task.Delay(100);
                    await JsInvokeAsync(JSInteropConstants.OverlayComponentHelper.DeleteOverlayFromContainer, Ref.Id);
                });
            }
            DomEventListener?.Dispose();
            base.Dispose(disposing);
        }

        internal async Task Show(int? overlayLeft = null, int? overlayTop = null)
        {
            if (_isOverlayShow || Trigger.Disabled)
            {
                return;
            }
            _isOverlayDuringShowing = true;

            if (_isOverlayFirstRender)
            {
                Trigger.SetShouldRender(false);
                await Task.Yield();
            }

            _overlayLeft = overlayLeft;
            _overlayTop = overlayTop;

            if (_isOverlayFirstRender)
            {
                _isWaitForOverlayFirstRender = true;

                await InvokeAsync(StateHasChanged);
            }

            await UpdateParentOverlayState(true);

            await AddOverlayToBody(overlayLeft, overlayTop);
            _isOverlayShow = true;
            _isOverlayDuringShowing = false;
            _isOverlayHiding = false;

            _overlayCls = Trigger.GetOverlayEnterClass();
            await Trigger.OnVisibleChange.InvokeAsync(true);

            if (Trigger != null && Trigger.VisibleChanged.HasDelegate)
            {
                await Trigger.VisibleChanged.InvokeAsync(true);
            }

            await InvokeAsync(StateHasChanged);

            if (OnShow.HasDelegate)
                _ = OnShow.InvokeAsync(null);
        }

        internal async Task Hide(bool force = false)
        {
            if (_isOverlayDuringShowing)
            {
                //If Show() method is processing, wait up to 1000 ms
                //for it to end processing
                await WaitFor(() => _isOverlayShow);
            }
            if (!_isOverlayShow)
            {
                return;
            }
            await Task.Delay(HideMillisecondsDelay);

            // if mouse is in overlay, don't hide
            // if there are children overlay is showing, don't hide
            // if trigger is set visible by outside and has no visible changed delegate, it means that the user is using visible to control show and hide, show don't hide by itself
            if (!force && ((!IsContainTrigger(TriggerType.Click) && _mouseInOverlay) || _isPreventHide || (_childrenToShow.Count > 0) || (Trigger.Visible && !Trigger.VisibleChanged.HasDelegate)))
            {
                return;
            }
            _isOverlayFirstRender = true;

            _isWaitForOverlayFirstRender = false;
            _isOverlayHiding = true;

            _overlayCls = Trigger.GetOverlayLeaveClass();

            await Trigger.OnOverlayHiding.InvokeAsync(true);
            await UpdateParentOverlayState(false);

            StateHasChanged();

            // wait for leave animation
            await Task.Delay(WaitForHideAnimMilliseconds);
            _isOverlayShow = false;
            _isOverlayHiding = false;

            await Trigger.OnVisibleChange.InvokeAsync(false);

            if (Trigger != null && Trigger.VisibleChanged.HasDelegate)
            {
                await Trigger.VisibleChanged.InvokeAsync(false);
            }

            StateHasChanged();

            if (OnHide.HasDelegate)
                _ = OnHide.InvokeAsync(null);
        }

        internal void PreventHide(bool prevent)
        {
            if (!prevent && _childrenToShow.Count > 0)
            {
                return;
            }
            _isPreventHide = prevent;
        }

        internal void SetMouseInOverlay(bool mouseInOverlay)
        {
            _mouseInOverlay = mouseInOverlay;
        }

        /// <summary>
        /// set if there any child overlay show or hide
        /// overlay would not hide if any child is showing
        /// </summary>
        /// <param name="child"></param>
        /// <param name="isChildOverlayShow"></param>
        internal void UpdateChildState(Overlay child, bool isChildOverlayShow)
        {
            if (isChildOverlayShow)
                AddVisibleChild(child);
            else
                RemoveVisibleChild(child);
        }

        internal void AddVisibleChild(Overlay child)
        {
            if (!_childrenToShow.Contains(child))
                _childrenToShow.Add(child);
        }

        internal void RemoveVisibleChild(Overlay child)
        {
            if (_childrenToShow.Contains(child))
                _childrenToShow.Remove(child);
        }

        internal bool IsPopup()
        {
            return _isOverlayShow;
        }

        /// <summary>
        /// when overlay is complete hide, IsPopup return true
        /// when overlay is hiding(playing hide animation), IsPopup return false, IsHiding return true.
        /// </summary>
        /// <returns></returns>
        internal bool IsHiding()
        {
            return _isOverlayHiding;
        }

        /// <summary>
        /// Indicates that a page is being refreshed
        /// </summary>
        private bool _isReloading;

        private OverlayPosition _position;

        private void Reloading(JsonElement jsonElement) => _isReloading = true;

        private int _recurenceGuard = 0;

        private async Task AddOverlayToBody(int? overlayLeft = null, int? overlayTop = null)
        {
            if (!_afterFirstRender)
            {
                return;
            }

            if (!_hasAddOverlayToBody)
            {
                bool triggerIsWrappedInDiv = Trigger.Unbound is null;
                _recurenceGuard++;

                //In ServerSide it may happen that trigger element reference has not yet been retrieved.
                if (!(await WaitFor(() => Trigger.Ref.Id is not null)))
                {
                    //Place where Error Boundary could be utilized
                    throw new ArgumentNullException("Trigger.Ref.Id cannot be null when attaching overlay to it.");
                }
                if (!(await WaitFor(() => Ref.Id is not null)))
                {
                    Debug.WriteLine("Overlay.Ref.Id is null. Adding overlay stopped.");
                    return;
                }

                _position = await JsInvokeAsync<OverlayPosition>(JSInteropConstants.OverlayComponentHelper.AddOverlayToContainer,
                    Ref.Id, Ref, Trigger.Ref, Trigger.Placement, Trigger.PopupContainerSelector,
                    Trigger.BoundaryAdjustMode, triggerIsWrappedInDiv, Trigger.PrefixCls,
                    VerticalOffset, HorizontalOffset, ArrowPointAtCenter, overlayTop, overlayLeft);
                if (_position is null && _recurenceGuard <= 10) //up to 10 attempts
                {
                    //Console.WriteLine($"Failed to add overlay to the container. Container: {Trigger.PopupContainerSelector}, trigger: {Trigger.Ref.Id}, overlay: {Ref.Id}. Awaiting and rerunning.");
                    await Task.Delay(10);
                    await AddOverlayToBody(overlayLeft, overlayTop);
                }
                else if (_position is not null)
                {
                    _hasAddOverlayToBody = true;
                    _overlayStyle = _position.PositionCss + GetTransformOrigin();
                    if (_position.Placement != Trigger.Placement)
                    {
                        Trigger.ChangePlacementForShow(PlacementType.Create(_position.Placement));
                    }
                }
            }
            else
            {
                await UpdatePosition(overlayLeft, overlayTop);
            }
            _recurenceGuard = 0;
        }

        /// <summary>
        /// Will probe a check predicate every given milliseconds until predicate is true or until
        /// runs out of number of probings.
        /// </summary>
        /// <param name="check">A predicate that will be run every time after waitTimeInMilisecondsPerProbing will pass.</param>
        /// <param name="probings">Maximum number of probings. After this number is reached, the method finishes.</param>
        /// <param name="waitTimeInMilisecondsPerProbing">How long to wait between each probing.</param>
        /// <returns>Task</returns>
        private async Task<bool> WaitFor(Func<bool> check, int probings = 100, int waitTimeInMilisecondsPerProbing = 10)
        {
            if (!check())
            {
                for (int i = 0; i < probings; i++)
                {
                    await Task.Delay(waitTimeInMilisecondsPerProbing);
                    if (check())
                    {
                        return true;
                    }
                }
                return false;
            }
            return true;
        }

        private string GetTransformOrigin()
        {
            return $"transform-origin: {Trigger.GetPlacementType().TranformOrigin}";
        }

        private bool IsContainTrigger(TriggerType triggerType)
        {
            foreach (TriggerType trigger in Trigger.GetTriggerType())
            {
                if (trigger == triggerType)
                {
                    return true;
                }
            }

            return false;
        }

        private async Task UpdateParentOverlayState(bool visible)
        {
            if (ParentTrigger == null)
            {
                return;
            }
            var parentOverlay = ParentTrigger.GetOverlayComponent();
            parentOverlay.UpdateChildState(this, visible);

            if (!visible)
            {
                if (parentOverlay.IsContainTrigger(TriggerType.Click) || parentOverlay.IsContainTrigger(TriggerType.ContextMenu))
                {
                    parentOverlay.PreventHide(true);
                    return;
                }
                await ParentTrigger.Hide();
            }
        }

        private string GetOverlayCls()
        {
            string overlayCls;

            if (!_isOverlayShow && !_isWaitForOverlayFirstRender)
            {
                overlayCls = Trigger.GetOverlayHiddenClass();
                _overlayCls = Trigger.GetOverlayEnterClass();
            }
            else
            {
                overlayCls = _overlayCls;
            }

            return overlayCls;
        }

        private string GetDisplayStyle()
        {
            if (!_isOverlayShow && !_isWaitForOverlayFirstRender)
                return "";

            if (_isOverlayShow && _hasAddOverlayToBody)
                return "display: inline-flex;";

            if (_hasAddOverlayToBody)
                return "visibility: hidden;";

            return "display: inline-flex; visibility: hidden;";
        }

        internal async Task UpdatePosition(int? overlayLeft = null, int? overlayTop = null)
        {
            bool triggerIsWrappedInDiv = Trigger.Unbound is null;

            _position = await JsInvokeAsync<OverlayPosition>(JSInteropConstants.OverlayComponentHelper.UpdateOverlayPosition,
                Ref.Id, Ref, Trigger.Ref, Trigger.Placement, Trigger.PopupContainerSelector,
                Trigger.BoundaryAdjustMode, triggerIsWrappedInDiv, Trigger.PrefixCls,
                VerticalOffset, HorizontalOffset, ArrowPointAtCenter, overlayTop, overlayLeft);
            if (_position is not null)
            {
                if (_position.Placement != Trigger.Placement)
                {
                    Trigger.ChangePlacementForShow(PlacementType.Create(_position.Placement));
                }
                _overlayStyle = _position.PositionCss + GetTransformOrigin();
            }
        }

        private decimal ChangeOverlayLeftToRight(int left, HtmlElement overlay, HtmlElement container)
        {
            return container.ClientWidth - left - overlay.OffsetWidth;
        }
    }
}
